//  Compiler for PHP (aka KPHP)
//  Copyright (c) 2024 LLC «V Kontakte»
//  Distributed under the GPL v3 License, see LICENSE.notice.txt

#include "runtime-light/stdlib/crypto/crypto-functions.h"

#include <algorithm>
#include <array>
#include <cctype>
#include <cstddef>
#include <functional>
#include <optional>
#include <ranges>
#include <string_view>
#include <utility>

#include "common/crc32_generic.h"
#include "common/tl/constants/common.h"
#include "runtime-common/core/allocator/script-allocator.h"
#include "runtime-common/core/runtime-core.h"
#include "runtime-common/core/std/containers.h"
#include "runtime-common/stdlib/server/url-functions.h"
#include "runtime-common/stdlib/string/string-functions.h"
#include "runtime-light/k2-platform/k2-api.h"
#include "runtime-light/stdlib/component/component-api.h"
#include "runtime-light/stdlib/diagnostics/logs.h"
#include "runtime-light/streams/read-ext.h"
#include "runtime-light/streams/stream.h"
#include "runtime-light/tl/tl-core.h"
#include "runtime-light/tl/tl-functions.h"
#include "runtime-light/tl/tl-types.h"

namespace {

constexpr std::string_view CRYPTO_COMPONENT_NAME = "crypto";

} // namespace

kphp::coro::task<Optional<string>> f$openssl_random_pseudo_bytes(int64_t length) noexcept {
  if (length <= 0 || length > string::max_size()) [[unlikely]] {
    co_return false;
  }

  tl::GetCryptosecurePseudorandomBytes get_pseudorandom_bytes{.size = {.value = static_cast<int32_t>(length)}};
  tl::storer tls{get_pseudorandom_bytes.footprint()};
  get_pseudorandom_bytes.store(tls);

  auto expected_stream{kphp::component::stream::open(CRYPTO_COMPONENT_NAME, k2::stream_kind::component)};
  if (!expected_stream) [[unlikely]] {
    co_return false;
  }

  auto stream{*std::move(expected_stream)};
  kphp::stl::vector<std::byte, kphp::memory::script_allocator> response{};
  if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), kphp::component::read_ext::append(response)))) [[unlikely]] {
    co_return false;
  }

  tl::fetcher tlf{response};
  tl::magic magic{};
  tl::string str{};
  kphp::log::assertion(magic.fetch(tlf) && magic.expect(TL_MAYBE_TRUE) && str.fetch(tlf));
  co_return string{str.value.data(), static_cast<string::size_type>(str.value.size())};
}

kphp::coro::task<Optional<array<mixed>>> f$openssl_x509_parse(string data, bool shortnames) noexcept {
  tl::GetPemCertInfo get_perm_cert_info{.is_short = shortnames, .bytes = {.value = {data.c_str(), data.size()}}};
  tl::storer tls{get_perm_cert_info.footprint()};
  get_perm_cert_info.store(tls);

  auto expected_stream{kphp::component::stream::open(CRYPTO_COMPONENT_NAME, k2::stream_kind::component)};
  if (!expected_stream) [[unlikely]] {
    co_return false;
  }

  auto stream{*std::move(expected_stream)};
  kphp::stl::vector<std::byte, kphp::memory::script_allocator> response_bytes{};
  if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), kphp::component::read_ext::append(response_bytes)))) [[unlikely]] {
    co_return false;
  }

  tl::fetcher tlf{response_bytes};
  tl::Maybe<tl::Dictionary<tl::CertInfoItem>> cert_items{};
  kphp::log::assertion(cert_items.fetch(tlf));
  if (!cert_items.opt_value) {
    co_return false;
  }

  array<mixed> response{array_size{static_cast<int64_t>((*cert_items.opt_value).size()), false}};
  auto item_to_mixed{tl::CertInfoItem::MakeVisitor{
      [](tl::i64 val) noexcept -> mixed { return val.value; }, [](tl::string val) noexcept -> mixed { return string(val.value.data(), val.value.size()); },
      [](const tl::dictionary<tl::string>& sub_dict) noexcept -> mixed {
        array<mixed> resp{array_size{static_cast<int64_t>(sub_dict.size()), false}};
        for (auto sub_item : sub_dict) {
          auto key{string{sub_item.key.value.data(), static_cast<string::size_type>(sub_item.key.value.size())}};
          auto value{string{sub_item.value.value.data(), static_cast<string::size_type>(sub_item.value.value.size())}};
          resp[key] = std::move(value);
        }
        return resp;
      }}};

  for (auto cert_kv : std::move(*cert_items.opt_value)) {
    auto key{string{cert_kv.key.value.data(), static_cast<string::size_type>(cert_kv.key.value.size())}};
    tl::CertInfoItem val{std::move(cert_kv.value)};
    response[string{cert_kv.key.value.data(), static_cast<string::size_type>(cert_kv.key.value.size())}] = std::visit(item_to_mixed, val.data);
  }
  co_return std::move(response);
}

// FIXME it isn't safe to accept signature by reference
kphp::coro::task<bool> f$openssl_sign(string data, string& signature, string private_key, int64_t algo) noexcept {
  tl::DigestSign digest_sign{.data = {.value = {data.c_str(), data.size()}},
                             .private_key = {.value = {private_key.c_str(), private_key.size()}},
                             .algorithm = static_cast<tl::HashAlgorithm>(algo)};
  tl::storer tls{digest_sign.footprint()};
  digest_sign.store(tls);

  auto expected_stream{kphp::component::stream::open(CRYPTO_COMPONENT_NAME, k2::stream_kind::component)};
  if (!expected_stream) [[unlikely]] {
    co_return false;
  }

  auto stream{*std::move(expected_stream)};
  kphp::stl::vector<std::byte, kphp::memory::script_allocator> response_bytes{};
  if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), kphp::component::read_ext::append(response_bytes)))) [[unlikely]] {
    co_return false;
  }

  tl::fetcher tlf{response_bytes};
  tl::magic magic{};
  tl::string str{};
  kphp::log::assertion(magic.fetch(tlf) && magic.expect(TL_MAYBE_TRUE) && str.fetch(tlf));
  signature = string{str.value.data(), static_cast<string::size_type>(str.value.size())};
  co_return true;
}

kphp::coro::task<int64_t> f$openssl_verify(string data, string signature, string pub_key, int64_t algo) noexcept {
  tl::DigestVerify digest_verify{.data = {.value = {data.c_str(), data.size()}},
                                 .public_key = {.value = {pub_key.c_str(), pub_key.size()}},
                                 .algorithm = static_cast<tl::HashAlgorithm>(algo),
                                 .signature = {.value = {signature.c_str(), signature.size()}}};
  tl::storer tls{digest_verify.footprint()};
  digest_verify.store(tls);

  auto expected_stream{kphp::component::stream::open(CRYPTO_COMPONENT_NAME, k2::stream_kind::component)};
  if (!expected_stream) [[unlikely]] {
    co_return 0;
  }

  auto stream{*std::move(expected_stream)};
  std::array<std::byte, tl::magic{}.footprint()> response{};
  if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), response))) [[unlikely]] {
    co_return 0;
  }

  tl::fetcher tlf{response};
  // For now returns only 1 or 0, -1 is never returned
  // Because it's currently impossible to distiguish error from negative verification
  if (tl::magic magic{}; !magic.fetch(tlf) || !magic.expect(TL_BOOL_TRUE)) {
    co_return 0;
  }
  co_return 1;
}

namespace {

constexpr std::string_view AES_128_CBC = "aes-128-cbc";
constexpr std::string_view AES_256_CBC = "aes-256-cbc";

std::optional<tl::CipherAlgorithm> parse_cipher_algorithm(const string& method) noexcept {
  using namespace std::string_view_literals;
  std::string_view method_sv{method.c_str(), method.size()};

  const auto ichar_equals = [](char a, char b) { return std::tolower(a) == std::tolower(b); };

  if (std::ranges::equal(method_sv, AES_128_CBC, ichar_equals)) {
    return tl::CipherAlgorithm::AES128;
  } else if (std::ranges::equal(method_sv, AES_256_CBC, ichar_equals)) {
    return tl::CipherAlgorithm::AES256;
  }
  return {};
}

constexpr size_t AES_BLOCK_LEN = 16;
constexpr size_t AES_128_KEY_LEN = 16;
constexpr size_t AES_256_KEY_LEN = 32;

int64_t algorithm_iv_len([[maybe_unused]] tl::CipherAlgorithm algorithm) noexcept {
  /* since only aes-128/256-cbc supported for now */
  return AES_BLOCK_LEN;
}

int64_t algorithm_key_len(tl::CipherAlgorithm algorithm) noexcept {
  switch (algorithm) {
  case tl::CipherAlgorithm::AES128: {
    return AES_128_KEY_LEN;
  }
  case tl::CipherAlgorithm::AES256: {
    return AES_256_KEY_LEN;
  }
  default: {
    kphp::log::warning("unexpected cipher algorithm");
    return 0;
  }
  }
}

enum class cipher_opts : int64_t { OPENSSL_RAW_DATA = 1, OPENSSL_ZERO_PADDING = 2, OPENSSL_DONT_ZERO_PAD_KEY = 4 };

Optional<std::pair<string, string>> algorithm_pad_key_iv(tl::CipherAlgorithm algorithm, const string& source_key, const string& source_iv,
                                                         int64_t options) noexcept {
  const size_t iv_required_len = algorithm_iv_len(algorithm);
  const size_t key_required_len = algorithm_key_len(algorithm);
  auto iv = source_iv;
  if (iv.size() < iv_required_len) {
    kphp::log::warning("IV passed is only {} bytes long, cipher expects an IV of precisely {} bytes, padding with \\0", iv.size(), iv_required_len);
    iv.append(static_cast<string::size_type>(iv_required_len - iv.size()), '\0');
  } else if (iv.size() > iv_required_len) {
    kphp::log::warning("IV passed is {} bytes long which is longer than the {} expected by selected cipher, truncating", iv.size(), iv_required_len);
    iv.shrink(static_cast<string::size_type>(iv_required_len));
  }

  auto key = source_key;
  if (key.size() < key_required_len) {
    if (options & static_cast<int64_t>(cipher_opts::OPENSSL_DONT_ZERO_PAD_KEY)) {
      kphp::log::warning("Key length should be {} bytes long:\n", key_required_len);
      return false;
    }
    kphp::log::warning("passphrase passed is only {} bytes long, cipher expects an passphrase of precisely {} bytes, padding with \\0", key.size(),
                       key_required_len);
    key.append(key_required_len - key.size(), '\0');
  } else if (key.size() > key_required_len) {
    kphp::log::warning("passphrase passed is {} bytes long which is longer than the {} expected by selected cipher, truncating", key.size(), key_required_len);
    key.shrink(static_cast<string::size_type>(key_required_len));
  }

  if (options & static_cast<int64_t>(cipher_opts::OPENSSL_ZERO_PADDING)) {
    kphp::log::warning("OPENSSL_ZERO_PADDING option is not supported for now\n");
    return false;
  }
  return std::make_pair(key, iv);
}

} // namespace

array<string> f$openssl_get_cipher_methods([[maybe_unused]] bool aliases) noexcept {
  array<string> return_value{
      {std::make_pair(0, string{AES_128_CBC.data(), AES_128_CBC.size()}), std::make_pair(1, string{AES_256_CBC.data(), AES_256_CBC.size()})}};
  return return_value;
}

Optional<int64_t> f$openssl_cipher_iv_length(const string& method) noexcept {
  auto algorithm{parse_cipher_algorithm(method)};
  if (!algorithm) {
    kphp::log::warning("Unknown cipher algorithm");
    return false;
  }
  return algorithm_iv_len(*algorithm);
}

kphp::coro::task<Optional<string>> f$openssl_encrypt(string data, string method, string source_key, int64_t options, string source_iv,
                                                     std::optional<std::reference_wrapper<string>> tag, string aad,
                                                     [[maybe_unused]] int64_t tag_length) noexcept {
  auto algorithm{parse_cipher_algorithm(method)};
  if (!algorithm) {
    kphp::log::warning("Unknown cipher algorithm");
    co_return false;
  }

  if (tag.has_value()) {
    kphp::log::warning("The authenticated tag cannot be provided for cipher that doesn not support AEAD");
  }
  if (!aad.empty()) {
    kphp::log::warning("The additional authenticated data cannot be provided for cipher that doesn not support AEAD");
  }
  if (source_iv.empty()) {
    kphp::log::warning("Using an empty Initialization Vector (iv) is potentially insecure and not recommended");
  }

  auto key_iv{algorithm_pad_key_iv(*algorithm, source_key, source_iv, options)};
  if (key_iv.is_null()) {
    co_return false;
  }

  tl::CbcEncrypt cbc_encrypt{.algorithm = *algorithm,
                             .padding = tl::BlockPadding::PKCS7,
                             .passphrase = {.value = {key_iv.val().first.c_str(), key_iv.val().first.size()}},
                             .iv = {.value = {key_iv.val().second.c_str(), key_iv.val().second.size()}},
                             .data = {.value = {data.c_str(), data.size()}}};
  tl::storer tls{cbc_encrypt.footprint()};
  cbc_encrypt.store(tls);

  auto expected_stream{kphp::component::stream::open(CRYPTO_COMPONENT_NAME, k2::stream_kind::component)};
  if (!expected_stream) [[unlikely]] {
    co_return false;
  }

  auto stream{*std::move(expected_stream)};
  kphp::stl::vector<std::byte, kphp::memory::script_allocator> response_bytes{};
  if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), kphp::component::read_ext::append(response_bytes)))) [[unlikely]] {
    co_return false;
  }

  tl::fetcher tlf{response_bytes};
  tl::String response{};
  kphp::log::assertion(response.fetch(tlf));
  string result{response.inner.value.data(), static_cast<string::size_type>(response.inner.value.size())};
  co_return (options & static_cast<int64_t>(cipher_opts::OPENSSL_RAW_DATA)) ? std::move(result) : f$base64_encode(result);
}

kphp::coro::task<Optional<string>> f$openssl_decrypt(string data, string method, string source_key, int64_t options, string source_iv, string tag,
                                                     string aad) noexcept {
  if (!(options & static_cast<int64_t>(cipher_opts::OPENSSL_RAW_DATA))) {
    Optional<string> decoding_data{f$base64_decode(data, true)};
    if (!decoding_data.has_value()) {
      kphp::log::warning("Failed to base64 decode the input");
      co_return false;
    }
    data = std::move(decoding_data.val());
  }

  auto algorithm{parse_cipher_algorithm(method)};
  if (!algorithm.has_value()) {
    kphp::log::warning("Unknown cipher algorithm");
    co_return false;
  }

  if (!tag.empty()) {
    kphp::log::warning("The authenticated tag cannot be provided for cipher that doesn not support AEAD");
  }
  if (!aad.empty()) {
    kphp::log::warning("The additional authenticated data cannot be provided for cipher that doesn not support AEAD");
  }

  auto key_iv{algorithm_pad_key_iv(*algorithm, source_key, source_iv, options)};
  if (key_iv.is_null()) {
    co_return false;
  }

  tl::CbcDecrypt cbc_decrypt{.algorithm = *algorithm,
                             .padding = tl::BlockPadding::PKCS7,
                             .passphrase = {.value = {key_iv.val().first.c_str(), key_iv.val().first.size()}},
                             .iv = {.value = {key_iv.val().second.c_str(), key_iv.val().second.size()}},
                             .data = {.value = {data.c_str(), data.size()}}};
  tl::storer tls{cbc_decrypt.footprint()};
  cbc_decrypt.store(tls);

  auto expected_stream{kphp::component::stream::open(CRYPTO_COMPONENT_NAME, k2::stream_kind::component)};
  if (!expected_stream) [[unlikely]] {
    co_return false;
  }

  auto stream{*std::move(expected_stream)};
  kphp::stl::vector<std::byte, kphp::memory::script_allocator> response_bytes{};
  if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), kphp::component::read_ext::append(response_bytes)))) [[unlikely]] {
    co_return false;
  }

  tl::fetcher tlf{response_bytes};
  tl::String response{};
  kphp::log::assertion(response.fetch(tlf));
  co_return string{response.inner.value.data(), static_cast<string::size_type>(response.inner.value.size())};
}

kphp::coro::task<Optional<string>> f$openssl_pkey_get_public(string key) noexcept {
  tl::GetPublicKey get_public_key{.key = {.value = {key.c_str(), key.size()}}};
  tl::storer tls{get_public_key.footprint()};
  get_public_key.store(tls);

  auto expected_stream{kphp::component::stream::open(CRYPTO_COMPONENT_NAME, k2::stream_kind::component)};
  if (!expected_stream) [[unlikely]] {
    co_return false;
  }

  auto stream{*std::move(expected_stream)};
  kphp::stl::vector<std::byte, kphp::memory::script_allocator> response_bytes{};
  if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), kphp::component::read_ext::append(response_bytes)))) [[unlikely]] {
    co_return false;
  }

  tl::fetcher tlf{response_bytes};
  tl::Maybe<tl::string> response;
  kphp::log::assertion(response.fetch(tlf));
  if (!response.opt_value) {
    co_return false;
  }

  co_return string{(*response.opt_value).value.data(), static_cast<string::size_type>((*response.opt_value).value.size())};
}

kphp::coro::task<Optional<string>> f$openssl_pkey_get_private(string key, string passphrase) noexcept {
  tl::GetPrivateKey get_private_key{
      .key = {.value = {key.c_str(), key.size()}},
      .passphrase = {.value = {passphrase.c_str(), passphrase.size()}},
  };
  tl::storer tls{get_private_key.footprint()};
  get_private_key.store(tls);

  auto expected_stream{kphp::component::stream::open(CRYPTO_COMPONENT_NAME, k2::stream_kind::component)};
  if (!expected_stream) [[unlikely]] {
    co_return false;
  }

  auto stream{*std::move(expected_stream)};
  kphp::stl::vector<std::byte, kphp::memory::script_allocator> response_bytes{};
  if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), kphp::component::read_ext::append(response_bytes)))) [[unlikely]] {
    co_return false;
  }

  tl::fetcher tlf{response_bytes};
  tl::Maybe<tl::string> response;
  kphp::log::assertion(response.fetch(tlf));
  if (!response.opt_value) {
    co_return false;
  }

  co_return string{(*response.opt_value).value.data(), static_cast<string::size_type>((*response.opt_value).value.size())};
}

kphp::coro::task<bool> f$openssl_public_encrypt(string data, string& encrypted_data, string public_key) noexcept {
  tl::PublicEncrypt public_encrypt{
      .key = {.value = {public_key.c_str(), public_key.size()}},
      .data = {.value = {data.c_str(), data.size()}},
  };
  tl::storer tls{public_encrypt.footprint()};
  public_encrypt.store(tls);

  auto expected_stream{kphp::component::stream::open(CRYPTO_COMPONENT_NAME, k2::stream_kind::component)};
  if (!expected_stream) [[unlikely]] {
    co_return false;
  }

  auto stream{*std::move(expected_stream)};
  kphp::stl::vector<std::byte, kphp::memory::script_allocator> response_bytes{};
  if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), kphp::component::read_ext::append(response_bytes)))) [[unlikely]] {
    co_return false;
  }

  tl::fetcher tlf{response_bytes};
  tl::String response{};
  kphp::log::assertion(response.fetch(tlf));
  encrypted_data = {response.inner.value.data(), static_cast<string::size_type>(response.inner.value.size())};
  co_return true;
}

kphp::coro::task<bool> f$openssl_public_encrypt(string data, mixed& result, string key) noexcept {
  string result_string;
  if (co_await f$openssl_public_encrypt(data, result_string, key)) {
    result = std::move(result_string);
    co_return true;
  }
  result = mixed{};
  co_return false;
}

kphp::coro::task<bool> f$openssl_private_decrypt(string data, string& decrypted_data, string private_key) noexcept {
  tl::PrivateDecrypt private_decrypt{
      .key = {.value = {private_key.c_str(), private_key.size()}},
      .data = {.value = {data.c_str(), data.size()}},
  };
  tl::storer tls{private_decrypt.footprint()};
  private_decrypt.store(tls);

  auto expected_stream{kphp::component::stream::open(CRYPTO_COMPONENT_NAME, k2::stream_kind::component)};
  if (!expected_stream) [[unlikely]] {
    co_return false;
  }

  auto stream{*std::move(expected_stream)};
  kphp::stl::vector<std::byte, kphp::memory::script_allocator> response_bytes{};
  if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), kphp::component::read_ext::append(response_bytes)))) [[unlikely]] {
    co_return false;
  }

  tl::fetcher tlf{response_bytes};
  tl::String response{};
  kphp::log::assertion(response.fetch(tlf));
  decrypted_data = {response.inner.value.data(), static_cast<string::size_type>(response.inner.value.size())};
  co_return true;
}

kphp::coro::task<bool> f$openssl_private_decrypt(string data, mixed& result, string key) noexcept {
  string result_string;
  if (co_await f$openssl_private_decrypt(data, result_string, key)) {
    result = std::move(result_string);
    co_return true;
  }
  result = mixed{};
  co_return false;
}

namespace {

constexpr std::array<std::pair<std::string_view, tl::HashAlgorithm>, 6> HASH_ALGOS = {{{"md5", tl::HashAlgorithm::MD5},
                                                                                       {"sha1", tl::HashAlgorithm::SHA1},
                                                                                       {"sha224", tl::HashAlgorithm::SHA224},
                                                                                       {"sha256", tl::HashAlgorithm::SHA256},
                                                                                       {"sha384", tl::HashAlgorithm::SHA384},
                                                                                       {"sha512", tl::HashAlgorithm::SHA512}}};

std::optional<tl::HashAlgorithm> parse_hash_algorithm(std::string_view user_algo) noexcept {
  const auto* it{std::ranges::find_if(
      HASH_ALGOS,
      [user_algo = std::ranges::transform_view(user_algo, [](auto c) { return std::tolower(c); })](auto hash_algo) noexcept {
        return std::ranges::equal(user_algo, hash_algo);
      },
      [](const auto& hash_algo) noexcept { return hash_algo.first; })};

  return it != nullptr && it != HASH_ALGOS.end() ? std::optional{it->second} : std::nullopt;
}

kphp::coro::task<string> send_and_get_string(tl::storer tls, bool raw_output) noexcept {
  auto expected_stream{kphp::component::stream::open(CRYPTO_COMPONENT_NAME, k2::stream_kind::component)};
  if (!expected_stream) [[unlikely]] {
    co_return false;
  }

  auto stream{*std::move(expected_stream)};
  kphp::stl::vector<std::byte, kphp::memory::script_allocator> response_bytes{};
  if (!co_await kphp::forks::id_managed(kphp::component::query(stream, tls.view(), kphp::component::read_ext::append(response_bytes)))) [[unlikely]] {
    co_return false;
  }

  tl::fetcher tlf{response_bytes};
  tl::String response{};
  kphp::log::assertion(response.fetch(tlf));

  if (!raw_output) {
    co_return kphp::strings::bin2hex(response.inner.value);
  }
  // Important to pass size because response.inner.value is binary so
  // it may contain zero in any position, not only in the end
  co_return string{response.inner.value.data(), static_cast<string::size_type>(response.inner.value.size())};
}

kphp::coro::task<string> hash_impl(tl::HashAlgorithm algo, string s, bool raw_output) noexcept {
  tl::Hash hash{.algorithm = algo, .data = {.value = {s.c_str(), s.size()}}};
  tl::storer tls{hash.footprint()};
  hash.store(tls);
  co_return co_await send_and_get_string(std::move(tls), raw_output);
}

} // namespace

array<string> f$hash_algos() noexcept {
  array<string> response{array_size{HASH_ALGOS.size(), true}};
  for (auto [algo_name, _] : HASH_ALGOS) {
    response.push_back(string{algo_name.data(), static_cast<string::size_type>(algo_name.size())});
  }
  return response;
}

array<string> f$hash_hmac_algos() noexcept {
  return f$hash_algos();
}

kphp::coro::task<string> f$hash(string algo_str, string s, bool raw_output) noexcept {
  const auto algo{parse_hash_algorithm({algo_str.c_str(), algo_str.size()})};
  if (!algo) [[unlikely]] {
    kphp::log::error("algo {} not supported in function hash", algo_str.c_str());
  }
  co_return co_await hash_impl(*algo, s, raw_output);
}

kphp::coro::task<string> f$hash_hmac(string algo_str, string s, string key, bool raw_output) noexcept {
  const auto algo{parse_hash_algorithm({algo_str.c_str(), algo_str.size()})};
  if (!algo) [[unlikely]] {
    kphp::log::error("algo {} not supported in function hash", algo_str.c_str());
  }

  tl::HashHmac hash_hmac{.algorithm = *algo, .data = {.value = {s.c_str(), s.size()}}, .secret_key = {.value = {key.c_str(), key.size()}}};
  tl::storer tls{hash_hmac.footprint()};
  hash_hmac.store(tls);
  co_return co_await send_and_get_string(std::move(tls), raw_output);
}

kphp::coro::task<string> f$sha1(string s, bool raw_output) noexcept {
  co_return co_await hash_impl(tl::HashAlgorithm::SHA1, s, raw_output);
}

int64_t f$crc32(const string& s) noexcept {
  return crc32_partial_generic(static_cast<const void*>(s.c_str()), s.size(), -1) ^ -1;
}
